package gui;

import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.RenderingHints;
import java.awt.event.*;
import java.awt.geom.*;
import java.awt.image.BufferedImage;
import java.util.*;

import javax.swing.*;
import org.scilab.forge.jlatexmath.*;

import fn.*;

public class PlotCanvas extends JPanel {

	private static final long serialVersionUID = -7137527730324719271L;

	private ArrayList<Function> fns = new ArrayList<Function>();
	private HashMap<Function, Color> colorMap = new HashMap<Function, Color>();
	private float lastHue = (float) Math.random();

	private ArrayList<Point> ps = new ArrayList<Point>();
	private HashMap<Point, Integer> typeMap = new HashMap<Point, Integer>();
	public static final int TYPE_NONE = -1, TYPE_XY = 0, TYPE_X = 1, TYPE_Y = 2;

	BufferedImage image;
	private float xPress, yPress;
	private View v = new View(this);

	// OPTIONS
	public static double _quality = 1;
	public Color _colAxis = new Color(0.0f, 0.0f, 0.0f, 0.7f);
	public Color _colNum = new Color(0.0f, 0.0f, 0.0f, 0.7f);
	public Color _colGrid = new Color(0.0f, 0.0f, 0.0f, 0.1f);
	public Color _colBack = new Color(1.0f, 1.0f, 1.0f, 1.0f);
	public boolean _showAxis = true;
	public boolean _showTick = false;
	public boolean _showNum = true;
	public boolean _showGrid = true;
	public boolean _showLgnd = false;

	private BasicStroke strokeThick = new BasicStroke(1.5f);
	private BasicStroke strokeNormal = new BasicStroke(1);

	public PlotCanvas(int w, int h) {
		setPreferredSize(new Dimension(w, h));
		addListeners();
		v.setZ(20);
		v.moveX(-w / 2);
		v.moveY(-h / 2);
		repaint();
	}

	@Override
	public void setBounds(int arg0, int arg1, int arg2, int arg3) {
		super.setBounds(arg0, arg1, arg2, arg3);
		image = new BufferedImage(arg2, arg3, BufferedImage.TYPE_INT_RGB);
	}

	private void addListeners() {
		addMouseListener(new MouseAdapter() {
			@Override
			public void mousePressed(MouseEvent e) {
				if (e.getButton() == MouseEvent.BUTTON3)
					resetView();
				xPress = e.getX();
				yPress = e.getY();
				repaint();
			}
		});
		addMouseMotionListener(new MouseAdapter() {
			@Override
			public void mouseDragged(MouseEvent e) {
				v.moveX(xPress - e.getX());
				v.moveY(yPress - e.getY());
				xPress = e.getX();
				yPress = e.getY();
				repaint();
			}
		});
		addMouseWheelListener(new MouseWheelListener() {
			@Override
			public void mouseWheelMoved(MouseWheelEvent arg0) {
				if (arg0.getWheelRotation() < -0.5f)
					v.zoomIn();
				else if (arg0.getWheelRotation() > 0.5f)
					v.zoomOut();
				repaint();
			}
		});
	}

	@Override
	public void paint(Graphics g) {
		Graphics2D g2 = (Graphics2D) image.getGraphics();
		g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
		// BACKDROP
		drawBackground(g2);
		// FUNCTIONS
		for (Function fn : fns)
			drawFn(g2, fn);
		// POINT
		for (Point p : ps) {
			g2.setColor(Color.getHSBColor(lastHue + 0.2f, 1f, 0.6f));
			drawPoint(g2, p);
		}
		// LEGEND
		int y = 0;
		if (_showLgnd)
			for (Function fn : fns)
				y = drawFnLegend(g2, fn, y);
		//
		g.drawImage(image, 0, 0, getWidth(), getHeight(), null);
	}

	private void drawBackground(Graphics2D g2) {
		// backdrop
		setBackground(_colBack);
		super.paint(g2);
		// vertical
		NiceScale.calculate(-v.yI(0), -v.yI(-getHeight()));
		drawYAxis(g2);
		drawHGrid(g2);
		// horizontal
		NiceScale.calculate(-v.xI(0), -v.xI(-getWidth()));
		drawXAxis(g2);
		drawVGrid(g2);
	}

	// VERTICAL

	private void drawYAxis(Graphics2D g2) {
		// AXIS
		g2.setColor(_colAxis);
		if (_showAxis)
			g2.drawLine((int) -v.x(0), 0, (int) -v.x(0), getHeight());
		// TICKS
		g2.setColor(_colAxis);
		if (_showTick)
			for (double y = NiceScale.min(); y < NiceScale.max(); y += NiceScale.spacing())
				g2.drawLine((int) -v.x(0) - 1, (int) -v.y(-y), (int) -v.x(0) + 1, (int) -v.y(-y));
		// NUMBERS
		g2.setColor(_colNum);
		if (_showNum)
			for (double y = NiceScale.min(); y < NiceScale.max(); y += NiceScale.spacing())
				if (Math.abs(y) > 0.0001)
					if (NiceScale.spacing() >= 1)
						Draw.string(g2, " " + (int) -y + " ", (int) -v.x(0), (int) -v.y(-y), (Math.signum(y) + 1), 0.5);
					else
						Draw.string(g2, " " + -Global.tol(y) + " ", (int) -v.x(0), (int) -v.y(-y), (Math.signum(y) + 1), 0.5);
	}

	private void drawHGrid(Graphics2D g2) {
		if (_showGrid) {
			g2.setColor(_colGrid);
			for (double y = NiceScale.min(); y < NiceScale.max(); y += NiceScale.spacing() / 2)
				g2.drawLine(0, (int) -v.y(-y), getWidth(), (int) -v.y(-y));
		}
	}

	// HORIZONTAL

	private void drawXAxis(Graphics2D g2) {
		// AXIS
		g2.setColor(_colAxis);
		if (_showAxis)
			g2.drawLine(0, (int) -v.y(0), getWidth(), (int) -v.y(0));
		// TICKS
		g2.setColor(_colAxis);
		if (_showTick)
			for (double x = NiceScale.min(); x < NiceScale.max(); x += NiceScale.spacing())
				g2.drawLine((int) -v.x(-x), (int) -v.y(0) - 1, (int) -v.x(-x), (int) -v.y(0) + 1);
		// NUMBERS
		g2.setColor(_colNum);
		if (_showNum)
			for (double x = NiceScale.min(); x < NiceScale.max(); x += NiceScale.spacing())
				if (Math.abs(x) > 0.0001)
					if (NiceScale.spacing() >= 1)
						Draw.string(g2, (int) x + "", (int) -v.x(-x), (int) -v.y(0), 0.5, Math.signum(x));
					else
						Draw.string(g2, Global.tol(x) + "", (int) -v.x(-x), (int) -v.y(0), 0.5, Math.signum(x));

	}

	private void drawVGrid(Graphics2D g2) {
		if (_showGrid) {
			g2.setColor(_colGrid);
			for (double x = NiceScale.min(); x < NiceScale.max(); x += NiceScale.spacing() / 2)
				g2.drawLine((int) -v.x(-x), 0, (int) -v.x(-x), getHeight());
		}
	}

	// FUNCTION

	private void drawFn(Graphics2D g, Function fn) {
		// CURVE
		GeneralPath p = new GeneralPath();
		double y;
		for (double x = 0; x <= getWidth(); x += _quality) {
			y = -v.y(fn.evaluate(-v.xI(-x)));
			// infinity, nan and overflow checks
			if (Double.isInfinite(y) || y < -getHeight() * 2 || y > getHeight() * 4)
				p.moveTo(x, y);
			else if (Double.isNaN(y))
				continue;
			else if (p.getCurrentPoint() == null)
				p.moveTo(x, y);
			// normal points
			else
				p.lineTo(x, y);
		}
		g.setStroke(strokeThick);
		g.setColor(colorMap.get(fn));
		g.draw(p);
		g.setStroke(strokeNormal);
	}

	private int drawFnLegend(Graphics2D g, Function fn, int y) {
		TeXIcon ti = Latex.create(fn, 15);
		g.setColor(new Color(1, 1, 1, 0.8f));
		g.fillRect(2, y, ti.getIconWidth() + 16, ti.getIconHeight() - 2);
		g.setColor(colorMap.get(fn));
		g.fillRect(4, y + ti.getIconHeight() / 2, 4, 4);
		ti.setForeground(Color.BLACK);
		ti.paintIcon(this, g, 12, y);
		return y + ti.getIconHeight();
	}

	public void addFn(Function fn) {
		if (fn == null)
			return;
		fns.add(fn);
		colorMap.put(fn, Color.getHSBColor(lastHue += 0.2f, 1f, 0.6f));
		repaint();
	}

	public void removeFn(Function fn) {
		fns.remove(fn);
		colorMap.remove(fn);
		lastHue -= 0.2f;
		repaint();
	}

	public void removeFnAll() {
		fns.clear();
		colorMap.clear();
		repaint();
	}

	// POINT

	private void drawPoint(Graphics2D g, Point p) {
		double x = -v.x(-p.x);
		double y = -v.y(p.y);
		g.setColor(g.getColor().brighter());
		switch (typeMap.get(p)) {
			case TYPE_NONE:
				break;
			case TYPE_X:
				g.drawLine((int) x, (int) y, (int) x, (int) -v.y(0));
				break;
			case TYPE_Y:
				g.drawLine((int) x, (int) y, (int) -v.x(0), (int) y);
				break;
			default:
				g.drawLine((int) x, (int) y, (int) x, (int) -v.y(0));
				g.drawLine((int) x, (int) y, (int) -v.x(0), (int) y);
				break;
		}
		g.setColor(g.getColor().darker());
		g.setStroke(strokeThick);
		drawCrosshair(g, (int) x, (int) y);
		g.setStroke(strokeNormal);
	}

	private void drawCrosshair(Graphics2D g, int x, int y) {
		g.drawLine(x - 4, y - 4, x + 4, y + 4);
		g.drawLine(x - 4, y + 4, x + 4, y - 4);
	}

	public void addPoint(Point p, int type) {
		if (Double.isNaN(p.y))
			p.y = fns.get(0).evaluate(p.x);
		ps.add(p);
		typeMap.put(p, type);
		repaint();
	}

	public void removePoint(Point p) {
		ps.remove(p);
		typeMap.remove(p);
		repaint();
	}

	public void removePointAll() {
		ps.clear();
		typeMap.clear();
		repaint();
	}

	// VIEW

	public void resetView() {
		if (Math.abs(v.x(0) + getWidth() / 2) < 0.01 && Math.abs(v.y(0) + getHeight() / 2) < 0.01)
			v.setZ(20);
		v.setX(0);
		v.setY(0);
		v.moveX(-getWidth() / 2);
		v.moveY(-getHeight() / 2);
		repaint();
	}

	public void moveX(int dX) {
		v.moveX(dX);
		repaint();
	}

	public void moveY(int dY) {
		v.moveX(dY);
		repaint();
	}

	// MAIN

	public static void main(String[] args) {
		JFrame f = new JFrame();
		f.setVisible(true);
		f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
		PlotCanvas p = new PlotCanvas(400, 400);
		p.addFn(Function.parse("x^4-x^3-x^2-2x+1"));
		p.addPoint(new Point(1, 1), TYPE_X);
		p.addPoint(new Point(-1, 1), TYPE_Y);
		p.addPoint(new Point(-2, -2), TYPE_XY);
		p.addPoint(new Point(2, -2), TYPE_NONE);
		f.add(p);
		f.pack();
	}
}
